Flutter 只是一个前端框架,无法提供一些系统能力,譬如蓝牙、相机、传感器等等,这就需要 Flutter 框架和原生的 Android/iOS 进行交互,来获取一些系统数据。为此,Flutter中提供了一个平台通道(platform channel),用于 Flutter 和原生平台的通信。平台通道正是 Flutter 和原生之间通信的桥梁,它也是 Flutter插件的底层基础设施。
接下来就以最新的方法,来一步步实现一个Flutter Plugin 插件,来达到和宿主系统的交互
之所以说最新的方法,是因为在我试验的过程中,搜了一些网上的例子(甚至是官方的文档),有一部分都还是旧的方法,一直出错,经过群友提醒,才找到正确的方法。
正是因为上面的这个原因,我还是先贴一下我的 Flutter 版本吧
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel beta, v1.17.0, on Microsoft Windows [Version 10.0.18362.836], locale zh-CN)
创建 Flutter Plugin
Flutter Plugin 本身也是一个 Flutter Project,直接在 AndroidStudio 中就可以创建,只不过是在 Create New Flutter Project
处选择 Flutter Plugin
即可:
在红圈处可以看到 Select a "Plugin" when exposing an Android or iOS API for developers
,就是我们要找的,然后项目名称和项目路径以及包名正常填写就可以,finish 完成创建。
先来看一下创建之后的目录结构:
主要注意以下几个部分:
- android:安卓平台相关的代码
- example:该插件使用的示例项目
- ios:iOS 平台相关的代码
- lib:里面的 dart 代码为插件代码
创建的 Plugin 可以直接运行,实际上运行的就是 example
里面的工程,它是一个调用原生系统 API,获取系统版本号的功能,其引用的就是我们创建的这个 Plugin,在 example 的 pubspec.yaml
中可以看到:
flutterplugintest:
# When depending on this package from a real application you should use:
# flutterplugintest: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
Flutter 端
我们来看一下插件代码,也就是 lib 目录下的 flutterplugintest.dart
:
import 'dart:async';
import 'package:flutter/services.dart';
class Flutterplugintest {
static const MethodChannel _channel =
const MethodChannel('flutterplugintest');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}
一共是两部分:
一个
MethodChannel
对象,其参数是一个字符串,用于指定该通道名称,需保证该通道名称的唯一性。一个
platformVersion
方法,在方法中通过调用MethodChannel
的invokeMethod
方法来调用原生的代码来获取系统版本号invokeMethod
方法参数是一个方法名。该方法是异步返回的,通过 Future 和 async 实现。
Android 端
Flutter 端的代码就是这么简单,再来看一下 Android 端的:
package com.example.flutterplugintest;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
/** FlutterplugintestPlugin */
public class FlutterplugintestPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutterplugintest");
channel.setMethodCallHandler(this);
}
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutterplugintest");
channel.setMethodCallHandler(new FlutterplugintestPlugin());
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}
直接查看 Android 端代码是飘红的,在 android
目录上右键 -- Flutter -- Open Android module in Android Studio 打开,就不飘红了。
代码是自动生成的,这里就需要说一下 Flutter 插件的一个坑了,Flutter 插件加载存在两个版本,一个是 FlutterPluginBinding
(版本 >= 1.12)一个是 Registrar
(版本 < 1.12),为了更好的兼容性,所以在 Android 端生成的代码中将两种方式都实现了一遍:
- FlutterPluginBinding 版本:
- onAttachedToEngine:插件被关联到 FlutterEngine 时调用,可以做一些初始化工作。可以看到在这个方法中设置了通道名称(该通道名称和 Flutter 端设置的相同),设置了方法回调。
- onMethodCall:处理从 Flutter 接收的指定方法调用。在这个方法中继续一些宿主平台的操作。需要注意的是,该方法是执行在 UI 线程的。
- onDetachedFromEngine:插件从 FlutterEngine 移除时调用,可以做一些清理工作。
- Registrar 版本:
- registerWith:为了兼容旧项目,详情查看 Upgrading pre 1.12 Android projects
如果插件还涉及到 Activity 和 Service,还需要实现:
- ActivityAware:用于 Activity 的生命周期管理和获取
- ServiceAware:用于 Service 的生命周期管理和获取
简单示例
我们实现一个弹出 Toast 的功能:
Flutter 端
在 Flutter 端设置 通道名称以及设置需要调用的 android 端方法名:
android_taost_plugin.dart
import 'dart:async';
import 'package:flutter/services.dart';
class AndroidToastPlugin {
///定义 MethodChannel,名称唯一,需要和 android 端口相同
static const MethodChannel _channel =
const MethodChannel('ANDROID_TOAST_PLUGIN');
///提供调用方法
static showToast() async {
///设置在 android 端调用的方法名
await _channel.invokeMethod('showToast');
}
}
Android 端
AndroidToastPlugin.java
package com.lixyz;
import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.view.FlutterView;
/**
* AndroidToastPlugin
*/
public class AndroidToastPlugin implements FlutterPlugin, MethodCallHandler {
private MethodChannel channel;
static Context context;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
//1.12 之后创建 MethodChannel,并设置名称,需要和 Flutter 端相同
channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "ANDROID_TOAST_PLUGIN");
//设置方法调用回调
channel.setMethodCallHandler(this);
//获取 Context,以显示 Toast 时使用
context = flutterPluginBinding.getApplicationContext();
}
/**
*该方法用于兼容 1.12 之前的版本
*和 onAttachedToEngine 方法执行相同的内容
*/
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "ANDROID_TOAST_PLUGIN");
channel.setMethodCallHandler(new AndroidToastPlugin());
context = registrar.context();
}
/**
*1.12 之后,方法调用时候的回调
*
*/
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
//响应 Flutter 中调用的方法
if (call.method.equals("showToast")) {
Toast.makeText(context,"这是 android 端发出的 Toast",Toast.LENGTH_SHORT).show();
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}
如何使用 Flutter Plugin 呢?
通过执行以下命令,可以在命令行中对此包进行正确性验证:
flutter packages pub publish --dry-run
验证完成之后,就可以使用或者发布了。
项目中直接引用
看一下项目中的 example
中的 main.dart
import 'package:flutter/material.dart';
import 'package:lixyz/android_taost_plugin.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: MaterialButton(
child: Text("点击弹出 Toast"),
color: Colors.blue,
onPressed: () {
AndroidToastPlugin.showToast();
},
),
),
),
);
}
}
调用插件的 showToast
方法,显示 Toast,就是这么简单。
发布到 Pub 仓库
---->[发布]----
flutter packages pub publish
---->[使用]----
dependencies:
ia_path: ^0.0.1
然后所有人都可以使用你写的插件了。
发布到 Github
当公司内部写的插件,只想在公司内部共享不想公布到公网上,那么也可以将插件发布到 github 的私有仓库中,然后在项目中使用:
dependencies:
flutter:
sdk: flutter
插件名(Flutter 插件项目中 yaml 中 name 属性指定的名称):
git:
url: github 地址,http:xxxxxxxx.git
需要注意的是,当使用 github 上公布的插件更新之后,使用该插件的工程想要获取最新版,需要先将 yaml 文件中的插件删除,然后
pub get
,再添加,再pub get
。